feat: add ssh port forwarding for remote previews - #2544
Conversation
|
Very nice, works great. One thing I noticed: When I click a URL for a started dev server in the terminal drawer and open it in the Emdash browser, the URL includes a trailing ) at the end. Because of that, the dev server doesn’t open correctly until I manually remove the ) from the URL. |
Greptile SummaryThis PR replaces the legacy host-rewriting dev-server detection approach with a full SSH port-forwarding pipeline. Terminal output is now scanned for local URLs, which are registered as either direct (
Confidence Score: 4/5Safe to merge for most users; two edge-case resource leaks in the port-forward layer are worth tracking but do not affect normal usage. The concurrent-restart path in
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/main/core/port-forwards/port-forward-service.ts | New service managing SSH port-forward tunnels; clean abstraction, but open has a same-ID concurrency window that can orphan a TCP server if called twice before either resolves. |
| apps/emdash-desktop/src/main/core/port-forwards/port-forward-tunnel.ts | Implements TCP-to-SSH-channel forwarding using node:net; correctly handles EADDRINUSE fallback to port 0 and cleans up sockets on close. |
| apps/emdash-desktop/src/main/core/preview-servers/preview-server-service.ts | Core orchestration of preview server lifecycle; forwardManual correctly adds the server before async ops to avoid teardown leaks. The SSH PTY-exit→reconnect path has a window where the server is transiently removed and may not re-appear if the remote dev server doesn't re-emit its URL. |
| apps/emdash-desktop/src/main/core/preview-servers/terminal-url-detector.ts | Parses and deduplicates local URLs from PTY output; correctly strips ANSI, normalizes hosts, and probes local ports with a consecutive-failure threshold. |
| apps/emdash-desktop/src/renderer/features/tasks/stores/preview-server-store.ts | Renderer-side MobX store with event-driven sync and optimistic local updates for stop/restart/forwardManual; workspace filtering is correct. |
| apps/emdash-desktop/src/renderer/features/tasks/components/preview-servers/preview-server-pill.tsx | Dropdown pill component for preview servers; correctly gates Open/Copy actions on canOpen/hasUrl and displays forwarding metadata. |
| apps/emdash-desktop/src/renderer/features/tasks/components/preview-servers/manual-forward-dialog.tsx | New dialog for manually forwarding a port; validates port range, handles async submission, and surfaces error messages. |
| apps/emdash-desktop/src/shared/core/preview-servers/types.ts | Shared type definitions for direct and forwarded preview servers; previewServerUrl correctly returns null when localPort is undefined. |
| apps/emdash-desktop/src/main/core/terminals/impl/ssh-terminal-provider.ts | Wires wireTerminalUrlDetector (replacing old dev-server-watcher) with probeLocalPorts: false and forwards detected URLs via the singleton previewServerService. |
| apps/emdash-desktop/src/main/core/terminals/impl/local-terminal-provider.ts | Wires URL detector for local terminals with probeLocalPorts: true; registers direct preview servers with host/port from detected output. |
| apps/emdash-desktop/src/shared/terminal-url.ts | Extracted from external-url.ts so the same normalization logic is usable in shared/main context; behavior is identical to the removed code. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant PTY as PTY (SSH/Local)
participant URLDet as TerminalUrlDetector
participant PSS as PreviewServerService
participant PFS as PortForwardService
participant Tunnel as PortForwardTunnel (node:net)
participant SSH as SSH Channel
participant Store as PreviewServerStore (renderer)
PTY->>URLDet: onData(chunk)
URLDet->>URLDet: "match URL pattern, parse & deduplicate"
URLDet->>PSS: onDetected(server)
PSS->>PSS: registerSshTarget / registerLocalTarget
PSS->>Store: emit upsert (status: starting)
PSS->>PFS: open(tunnelId, remotePort)
PFS->>Tunnel: bind TCP server on localhost:preferredPort
Tunnel->>SSH: forwardOut(remotePort)
Tunnel-->>PFS: localPort
PFS-->>PSS: PortForwardRecord
PSS->>Store: emit upsert (status: ready, localPort)
Note over PTY,Store: Manual forward path
Store->>PSS: forwardManual(connectionId, remotePort)
PSS->>Store: emit upsert (status: starting)
PSS->>PFS: open(tunnelId, remotePort)
PFS-->>PSS: PortForwardRecord
PSS->>Store: emit upsert (status: ready, localPort)
Note over PTY,Store: SSH connection events
SSH-->>PSS: handleSshConnectionEvent(reconnected/failed)
PSS->>Store: emit upsert (status: ready/reconnecting/failed)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant PTY as PTY (SSH/Local)
participant URLDet as TerminalUrlDetector
participant PSS as PreviewServerService
participant PFS as PortForwardService
participant Tunnel as PortForwardTunnel (node:net)
participant SSH as SSH Channel
participant Store as PreviewServerStore (renderer)
PTY->>URLDet: onData(chunk)
URLDet->>URLDet: "match URL pattern, parse & deduplicate"
URLDet->>PSS: onDetected(server)
PSS->>PSS: registerSshTarget / registerLocalTarget
PSS->>Store: emit upsert (status: starting)
PSS->>PFS: open(tunnelId, remotePort)
PFS->>Tunnel: bind TCP server on localhost:preferredPort
Tunnel->>SSH: forwardOut(remotePort)
Tunnel-->>PFS: localPort
PFS-->>PSS: PortForwardRecord
PSS->>Store: emit upsert (status: ready, localPort)
Note over PTY,Store: Manual forward path
Store->>PSS: forwardManual(connectionId, remotePort)
PSS->>Store: emit upsert (status: starting)
PSS->>PFS: open(tunnelId, remotePort)
PFS-->>PSS: PortForwardRecord
PSS->>Store: emit upsert (status: ready, localPort)
Note over PTY,Store: SSH connection events
SSH-->>PSS: handleSshConnectionEvent(reconnected/failed)
PSS->>Store: emit upsert (status: ready/reconnecting/failed)
Reviews (2): Last reviewed commit: "fix(preview): avoid focusing disabled pr..." | Re-trigger Greptile
…rwarding feat: add ssh port forwarding for remote previews
#2458